Java内置锁synchronized的实现原理及应用(三)
简述
Java中每个对象都可以用来实现一个同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
具体表现形式如下:
1、普通同步方法,锁的是当前实例对象
2、静态同步方法,锁的是当前Class对象
3、对于同步代码块,锁的是Synchronized括号中的代码块
线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时自动释放锁,无论是通过正常路径退出,还是通过代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步方法或代码块。
从 JVM 规范 中 可以 看到 Synchonized 在 JVM 里 的 实现 原理, JVM 基于 进入 和 退出 Monitor 对象 来 实现 方法 同步 和 代码 块 同步, 但 两者 的 实现 细节 不一样。 代码 块 同步 是 使用 monitorenter 和 monitorexit 指令 实现 的, 而 方法 同步 是 使用 另外 一种 方式 实现 的, 细节 在 JVM 规范 里 并没有 详细 说明。 但是, 方法 的 同步 同样 可以 使用 这 两个 指令 来 实现。
monitorenter 指令 是在 编译 后 插入 到 同步 代码 块 的 开始 位置, 而 monitorexit 是 插入 到 方法 结束 处 和 异常 处, JVM 要 保证 每个 monitorenter 必须 有 对应 的 monitorexit 与之 配对。 任何 对象 都有 一个 monitor 与之 关联, 当 且 一个 monitor 被 持有 后, 它将 处于 锁定 状态。 线程 执行 到 monitorenter 指令时, 将会 尝试 获取 对象 所 对应 的 monitor 的 所有权, 即 尝试 获得 对象 的 锁。
对象头
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Heather)、实例数据(Instance Data)和对齐填充(Padding)。
synchronized 用的 锁 是 存在 对象头 中。
如果 对象 是 数组 类型, 则 虚拟 机 用 3 个 字 宽( Word) 存储 对 象头, 如果 对象 是非 数组 类型, 则用 2 字 宽 存储 对 象头。 在 32 位 虚拟 机中, 1 字 宽 等于 4 字节, 即 32bit。
Java 对象 头里 的 Mark Word 里 默认 存储 对象 的 HashCode、 分 代 年龄 和 锁 标记 位。
锁升级
Java SE 1. 6 为了 减少 获得 锁 和 释放 锁 带来 的 性能 消耗, 引入 了“ 偏向 锁” 和“ 轻量级 锁”, 在 Java SE 1. 6 中, 锁 一 共有 4 种 状态, 级别 从低 到 高 依次 是: 无 锁 状态、 偏向 锁 状态、 轻量级 锁 状态 和 重量级 锁 状态, 这 几个 状态 会 随着 竞争 情况 逐渐 升级。 锁 可以 升级 但不能 降级, 意味着 偏向 锁 升级 成 轻量级 锁 后 不能 降级 成 偏向 锁。 这种 锁 升级 却不 能 降级 的 策略, 目的 是 为了 提高 获得 锁 和 释放 锁 的 效率。
偏向锁
HotSpot的作者经过研究发现,大多情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,当该线程再次进入和退出同步块时,不需要再进行CAS操作加锁和解锁,只需要判断一下对象头中(Mark Word)是否存储有指向当前线程的偏向锁。
如果存在指向当前线程的偏向锁,说明该线程已经获得了锁。
如果不存在,则需要判断一下Mark Word中的偏向锁标识是否为1,如果不为1,就使用CAS竞争锁;如果标识为1,则尝试使用CAS将对象头中的偏向锁指向当前线程。
1、偏向锁撤销
偏向锁使用了一种等到锁竞争出现才释放锁的机制,当其它线程竞争偏向锁时,当前持有偏向锁的线程才会释放锁。
偏向锁的撤销,需要等待全局安全点(在这个时间点没有正在执行的字节码)。此时会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程处于非活动状态,则设置对象头锁标识为无锁状态;如果线程依然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word 要么重新偏向于其它线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
2、关闭偏向锁
偏向锁在Java6和Java7中是默认启用的,它在应用程序启动几秒钟之后才激活,如有需要可以使用JVM参数来关闭延迟或关闭偏向锁:
-XX:BiasedLockingStartupDelay=0 关闭偏向锁激活延迟
-XX:-UseBiasedLocking=false 关闭偏向锁(程序默认进入轻量级锁)
轻量级锁
1、轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程舱室使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果替换成功当前线程获得锁,如果失败,表示其他线程竞争 锁,当前线程就会尝试使用自旋锁来获取锁。
2、轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作,将Displaced Mark Word 替换回到对象头,如果替换成功,表示没有竞争发生;如果替换失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程,阻塞了),一旦锁升级成重量级锁,就不会再降级到轻量级锁了。此时,其它线程试图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些阻塞的线程,被唤醒的线程就会进行新一轮的竞争锁。
锁的优缺点对比
synchronized 锁重入
关键字 synchronized 拥有锁重入的功能,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也说明了,在一个synchronized 方法或代码块的内部调用当前对象的其它 synchronized 方法或代码块时,始终时可以得到锁的。
public class Class1 {
synchronized public void method1() {
System.out.println(" method1");
method2();
}
synchronized public void method2() {
System.out.println(" method2");
method3();
}
synchronized public void method3() {
System.out.println(" method3");
}
}
public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
Class1 class1=new Class1();
class1.method1();
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
输出结果:
method1
method2
method3
由此结果可以得出以下结论:一个线程获得了某个对象的锁后,当该线程持有这个对象的锁还没有释放的情况下,该线程再次调用当前对象中另外一个同步方法时,这个对象的锁还是可以获取的。如果锁不可重入的话,就会造成死锁。
synchronized 锁,出现异常自动释放
当 一个 线程 执行 的 代码 出现 异常 时, 其所 持 有的 锁 会 自动 释放。
public class Class1 {
synchronized public void method1() {
System.out.println(" method1");
method2();
}
synchronized public void method2() {
System.out.println(" method2");
method3();
}
synchronized public void method3() {
System.out.println(" method3");
}
synchronized public void testMethod() {
if (" a".equals(Thread.currentThread().getName())) {
while (true) {
System.out.println(" ThreadName=" + Thread.currentThread().getName() + " 执行开始时间=" + System.currentTimeMillis());
System.out.println(" ThreadName=" + Thread.currentThread().getName() + " 出现异常" + System.currentTimeMillis());
Integer.parseInt(" a");
}
} else {
System.out.println(" Thread B 执行开始时间=" + System.currentTimeMillis());
}
}
}
public class ThreadA extends Thread {
private Class1 class1;
public ThreadA(Class1 class1) {
this.class1 = class1;
}
@Override
public void run() {
class1.testMethod();
}
}
public class ThreadB extends Thread {
private Class1 class1;
public ThreadB(Class1 class1) {
this.class1 = class1;
}
@Override
public void run() {
class1.testMethod();
}
}
public static void main(String[] args) {
Class1 class1 = new Class1();
ThreadA a = new ThreadA(class1);
a.setName(" a");
a.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadB b = new ThreadB(class1);
b.setName(" b");
b.start();
}
执行结果:
ThreadName= a 执行开始时间=1535899339871
ThreadName= a 出现异常1535899339871
Exception in thread " a" java.lang.NumberFormatException: For input string: " a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:569)
at java.lang.Integer.parseInt(Integer.java:615)
at com.lkf.Class1.testMethod(Class1.java:29)
at com.lkf.ThreadA.run(ThreadA.java:20)
Thread B 执行开始时间=1535899340375
线程A出现异常并释放锁,线程B进入方法,正常执行,结论就是出现异常锁会自动释放
synchronized 锁,同步不具有继承性
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { }
}
synchronized 关键字修饰的方法被重写后默认不再是 synchronized 的;虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,所以如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,必须显式地在子类的这个方法中加上 synchronized 关键字才可以。
总结
1、synchronized 关键字主要用来解决多线程并发同步问题,可以用来修饰类的实例方法、静态方法、代码块;
2、synchronized 实例方法实际保护的是同一个对象的方法调用,当为不同对象时多线程是可以同时访问同一个 synchronized 方法的;
3、synchronized 静态方法和 synchronized 实例方法保护的是不同对象,不同的两个线程可以同时执行 synchronized 静态方法,另一个执行 synchronized 实例方法,因为 synchronized 静态方法保护的是 class 类对象,synchronized 实例方法保护的是 this 实例对象;
4、synchronized 代码块同步的可以是任何对象,因为任何对象都有一个锁和等待队列。
5、synchronized 具备可重入性,对同一个线程在获得锁之后在调用其他需要同样锁的代码时可以直接调用,其可重入性是通过记录锁的持有线程和持有数量来实现的,调用 synchronized 代码时检查对象是否已经被锁,是则检查是否被当前线程锁定,是则计数加一,不是则加入等待队列,释放时计数减一直到为零释放锁。
6、synchronized 还具备内存可见性,除了实现原子操作避免竞态以外对于明显是原子操作的方法(譬如一个 boolean 状态变量 state 的 get 和 set 方法)也可以通过 synchronized 来保证并发的可见性,在释放锁时所有写入都会写回内存,而获得锁后都会从内存读取最新数据;不过对于已经是原子性的操作为了保证内存可见性而使用 synchronized 的成本会比较高,轻量级的选择应该是使用 volatile 修饰,一旦修饰 java 就会在操作对应变量时插入特殊指令保证可见性。
7、synchronized 是重量级锁,其语义底层是通过一个 monitor 监视器对象来完成,其实 wait、notify 等方法也依赖于 monitor 对象,所以这就是为什么只有在同步的块或者方法中才能调用 wait、notify 等方法,否则会抛出 IllegalMonitorStateException 异常的原因,监视器锁(monitor)的本质依赖于底层操作系统的互斥锁(Mutex Lock)实现,而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,所以这就是为什么 synchronized 效率低且重量级的原因(Java 1.6 进行了优化,但是相比其他锁机制还是略显偏重)。
8、synchronized 在发生异常时会自动释放线程占用的锁资源,Lock 需要在异常时主动释放,synchronized 在锁等待状态下无法响应中断而 Lock 可以。